/***************************************************************************
 *
 * $Id: main.c 2 2009-08-02 11:30:22Z Michael.McTernan $
 *
 * This file is part of tdgen, a timing diagram renderer.
 * Copyright (C) 2005 Michael C McTernan, Michael.McTernan.2001@cs.bris.ac.uk
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
 **************************************************************************/

/*****************************************************************************
 * Header Files
 *****************************************************************************/

#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <stdio.h>
#include "cmdparse.h"
#include "licence.h"
#include "adraw.h"
#include "tig.h"

/*****************************************************************************
 * Preprocessor Macros & Constants
 *****************************************************************************/

#define M_Max(a, b) ((a) > (b) ? (a) : (b))

/*****************************************************************************
 * Typedefs
 *****************************************************************************/

/*****************************************************************************
 * Local Variable Definitions
 *****************************************************************************/


static bool gInputFilePresent = false;
static char gInputFile[4096];

static bool gOutputFilePresent = false;
static char gOutputFile[4096];

static bool gOutTypePresent = false;
static char gOutType[10];

static bool gDumpLicencePresent = false;

static bool gPrintParsePresent = false;

/** Command line switches.
 * This gives the command line switches that can be interpreted by mscgen.
 */
static CmdSwitch gClSwitches[] =
{
    {"-i",     &gInputFilePresent,  "%4096[^?]", gInputFile },
    {"-o",     &gOutputFilePresent, "%4096[^?]", gOutputFile },
    {"-T",     &gOutTypePresent,    "%10[^?]",   gOutType },
    {"-l",     &gDumpLicencePresent,NULL,        NULL },
    {"-p",     &gPrintParsePresent, NULL,        NULL }
};

/** Name of a file to be removed by deleteTmp(). */
static char *deleteTmpFilename = NULL;

static struct
{
    uint16_t    border;
    uint16_t    rowSep;
    uint16_t    rowHeight;
    uint16_t    labelSep;
    uint16_t    columnWidth;
    uint16_t    divs;           /**< Divisions every n columns. */
    ADrawColour zColour;
}
gOpt =
{
    8,
    8,
    16,
    4,
    16,
    1,
    ADRAW_COL_BLUE
};

/*****************************************************************************
 * Global Variable Definitions
 *****************************************************************************/

/*****************************************************************************
 * Local Function Definitions
 *****************************************************************************/

/** Delete the file named in deleteTmpFilename.
 * This function is registered with atexit() to delete a possible temporary
 * file used when generating image map files.
 */
static void deleteTmp()
{
    if(deleteTmpFilename)
    {
        unlink(deleteTmpFilename);
    }
}

static void trimSpace(char *s)
{
    uint16_t f = 0, l = strlen(s);

    /* Trim trailing space */
    while(l > 0 && isspace(s[l - 1])) l--;
    s[l] = '\0';

    while(isspace(s[f])) f++;

    /* Shift the string down */
    memmove(s, &s[f], (l + 1) - f);
}

static void usage(void)
{
    printf("Usage: tdgen -T <type> -i <input> [-o <output>]\n"
           "       tdgen -T <type> [-o <output>] [-i] <input>\n"
           "       tdgen -l\n"
           "\n"
           "Where:\n"
           "  -T <type>   Selects out output file format which maybe one of png, svn or \n"
           "               eps.\n"
           "  -i <input>  Input file giving timing commands.\n"
           "  -o <output> Output file name.  Can be omitted if an input filename is given,\n"
           "               in which case the output file name is taken from the input\n"
           "               filename with the output file type suffix added.\n"
           "  -l          Display program licence and exit.\n"
           "\n"
           "Timgen, a timing diagram renderer.\n"
           "Copyright (C) 2009 Michael C McTernan, Michael.McTernan.2001@cs.bris.ac.uk\n"
           "\n"
           "Timgen comes with ABSOLUTELY NO WARRANTY.  This is free software, and you are\n"
           "welcome to redistribute it under certain conditions; type `tdgen -l' for\n"
           "details.\n"
           "\n"
           "PNG rendering by libgd, www.libgd.org\n"
           "\n"
           );
}

static void computeDimensions(const tig_wave_t *tw,
                              ADraw            *drw,
                              uint32_t *const   w,
                              uint32_t *const   h,
                              uint16_t *const   maxLabelWidth,
                              uint16_t *const   maxPatternWidth)
{
    uint16_t rows = 0;

    *maxLabelWidth = 0;
    *maxPatternWidth = 0;

    /* Compute the width */
    while(tw)
    {
        rows++;
        *maxLabelWidth   = M_Max(*maxLabelWidth,
                                 drw->textWidth(drw, tw->label));
        *maxPatternWidth = M_Max(*maxPatternWidth,
                                  strlen(tw->pattern) * gOpt.columnWidth);

        tw = tw->next;
    }

    /* Compute the height */
    *h = rows * (gOpt.rowSep + gOpt.rowHeight) + gOpt.rowSep;
    *w = *maxLabelWidth + *maxPatternWidth + gOpt.labelSep;

    *h += gOpt.border * 2;
    *w += gOpt.border * 2;
}

static void plotLabel(ADraw   *drw,
                      char    *label,
                      uint16_t labelLen,
                      uint16_t startX,
                      uint16_t endX,
                      uint16_t y)
{
    if(labelLen != 0)
    {
        int yt = y + (drw->textHeight(drw) / 2);

        /* Null terminate, remove spaces */
        label[labelLen] = '\0';
        trimSpace(label);

        /* Check if the string was completely stripped */
        if(*label != '\0')
        {
            drw->textC(drw, (startX + endX) / 2, yt, label);
        }
    }
}


/** Determine the start state for some pattern input.
 * This scans an input line for the first transition character such that the
 * starting state can be determined.
 *
 * \param[in] pattern   The input pattern to parse.
 * \param[in,out] top   Top line starting state pointer.
 * \param[in,out] mid   Mid line starting state pointer.
 * \param[in,out] bot   Bottom line starting state pointer.
 */
static void plotPatternStartState(const char *pattern,
                                  bool *const top,
                                  bool *const mid,
                                  bool *const bot)
{
    bool retry;

    *top = *mid = *bot = false;

    do
    {
        retry = false;

        /* Skip white space */
        while(isspace(*pattern) && *pattern != '\0')
        {
            pattern++;
        }

        /* Check for a transition character or end of line */
        switch(*pattern)
        {
            case '<': *mid = true;          break;
            case '=':
            case 'X':
            case '>': *top = *bot = true;   break;
            case '_':
            case '/': *bot = true;          break;
            case '`':
            case '\\': *top = true;         break;
            case '-': *mid = true;          break;
            case '\0':                      break;
            default: retry = true;          break;
        }

        pattern++;
    }
    while(retry);
}

static void plotPattern(ADraw           *drw,
                        uint32_t         x,
                        uint32_t         y,
                        const char      *pattern)
{
    const uint32_t len = strlen(pattern);
    const uint32_t yt  = y - gOpt.rowHeight;
    const uint32_t yb  = y;
    const uint32_t ym  = y - (gOpt.rowHeight / 2);
    uint32_t       t;
    bool           top, mid, bot;
    uint16_t       labelLen = 0, labelStartX = x + gOpt.columnWidth / 8;
    char           label[80];

    /* Work out if the top, middle or bottow traces are active */
    plotPatternStartState(pattern, &top, &mid, &bot);

    /* Traverse the pattern plotting transitions */
    for(t = 0; t < len; t++)
    {
        const uint32_t ex = x + gOpt.columnWidth / 8;
        const uint32_t qx = x + gOpt.columnWidth / 4;
        const uint32_t hx = x + gOpt.columnWidth / 2;
        const uint32_t nx = x + gOpt.columnWidth;

        /* Switch for rendering transitions */
        switch(pattern[t])
        {
            case '/': case '`':
                if(top) { drw->line(drw, x, yt, nx, yt); }
                if(mid) { drw->line(drw, x, ym, ex, yt); drw->line(drw, ex, yt, qx, yt); }
                if(bot) { drw->line(drw, x, yb, qx, yt); }
                drw->line(drw, qx, yt, nx, yt);
                top = true; mid = bot = false;
                break;
            case '\\': case '_':
                if(top) { drw->line(drw, x, yt, qx, yb); }
                if(mid) { drw->line(drw, x, ym, ex, yb); drw->line(drw, ex, yb, qx, yb); }
                if(bot) { drw->line(drw, x, yb, nx, yb); }
                drw->line(drw, qx, yb, nx, yb);
                top = mid = false; bot = true;
                break;
            case '-': case '>':
                if(top) { drw->line(drw, x, yt, ex, ym); drw->line(drw, ex, ym, nx, ym); }
                if(mid) { drw->setPen(drw, gOpt.zColour); drw->line(drw, x, ym, qx, ym); }
                if(bot) { drw->line(drw, x, yb, ex, ym); drw->line(drw, ex, ym, nx, ym); }
                drw->setPen(drw, gOpt.zColour);
                drw->line(drw, qx, ym, nx, ym);
                drw->setPen(drw, ADRAW_COL_BLACK);
                top = bot = false; mid = true;
                break;
            case '<': case 'X':
                if(top) { drw->line(drw, x, yt, qx, yt); drw->line(drw, x, yt, qx, yb); }
                if(mid)
                {
                    drw->setPen(drw, gOpt.zColour);
                    drw->line(drw, x, ym, ex, ym);
                    drw->setPen(drw, ADRAW_COL_BLACK);
                    drw->line(drw, ex, ym, qx, yt); drw->line(drw, ex, ym, qx, yb);
                }
                if(bot) { drw->line(drw, x, yb, qx, yb); drw->line(drw, x, yb, qx, yt); }
                drw->line(drw, qx, yt, nx, yt);
                drw->line(drw, qx, yb, nx, yb);
                top = bot = true; mid = false;
                break;
            case '=':
                if(top)
                {
                    drw->line(drw, x, yt, nx, yt);
                    if(!bot) { drw->line(drw, x, yt, qx, yb); drw->line(drw, qx, yb, nx, yb); }
                }
                if(mid)
                {
                    drw->setPen(drw, gOpt.zColour);
                    drw->line(drw, x, ym, ex, ym);
                    drw->setPen(drw, ADRAW_COL_BLACK);
                    drw->line(drw, ex, ym, qx, yt); drw->line(drw, ex, ym, qx, yb);
                    drw->line(drw, qx, yt, nx, yt); drw->line(drw, qx, yb, nx, yb);
                }
                if(bot)
                {
                    drw->line(drw, x, yb, nx, yb);
                    if(!top) { drw->line(drw, x, yb, qx, yt); drw->line(drw, qx, yt, nx, yt); }
                }
                top = bot = true; mid = false;
                break;
            case '~':
                if(top) { drw->line(drw, x, yt, hx - 2, yt); drw->line(drw, hx + 4, yt, nx, yt); }
                if(mid)
                {
                    drw->setPen(drw, gOpt.zColour);
                    drw->line(drw, x, ym, hx - 2, ym);
                    drw->line(drw, hx + 2, ym, nx, ym);
                    drw->setPen(drw, ADRAW_COL_BLACK);
                }
                if(bot) { drw->line(drw, x, yb, hx - 4, yb); drw->line(drw, hx + 2, yb, nx, yb); }

                /* Wavy lines */
                for(int8_t a = 2; a > -4; a -= 4)
                {
                    drw->arc(drw, x + 2 + a, yt, gOpt.columnWidth, gOpt.columnWidth, 270 + 45, 0);
                    drw->line(drw, hx + 2 + a, yt, hx - 2 + a, yb);
                    drw->arc(drw, nx - 2 + a, yb, gOpt.columnWidth, gOpt.columnWidth, 180 - 45, 180);
                }
                break;
            case '.':
                if(top) drw->dottedLine(drw, x, yt, nx, yt);
                if(mid)
                {
                    drw->setPen(drw, gOpt.zColour);
                    drw->dottedLine(drw, x, ym, nx, ym);
                    drw->setPen(drw, ADRAW_COL_BLACK);
                }
                if(bot) drw->dottedLine(drw, x, yb, nx, yb);
                break;
            default:
            case ' ':
                if(top) drw->line(drw, x, yt, nx, yt);
                if(mid)
                {
                    drw->setPen(drw, gOpt.zColour);
                    drw->line(drw, x, ym, nx, ym);
                    drw->setPen(drw, ADRAW_COL_BLACK);
                }
                if(bot) drw->line(drw, x, yb, nx, yb);
                break;
        }


        switch(pattern[t])
        {
            case '/': case '`': case '\\': case '_':
            case '-': case '>': case '<': case 'X':
            case '=': case '~':
                plotLabel(drw, label, labelLen, labelStartX, x, ym);

                labelStartX = ex;
                labelLen = 0;

                break;
            case ' ':
            default:
                /* Store the character */
                label[labelLen] = pattern[t];
                labelLen++;
                break;

            case '.':
                /* Ignore dots so text aligns though it */
                break;
        }

        x = nx;
    }

    plotLabel(drw, label, labelLen, labelStartX, x, ym);

}

static void plot(tig_wave_t      *tw,
                 ADraw           *drw,
                 uint32_t         w,
                 uint32_t         h,
                 uint16_t         maxLabelWidth)
{
    uint16_t x, y = gOpt.border + gOpt.rowSep + gOpt.rowHeight;

    if(gOpt.divs != 0)
    {
        drw->setPen(drw, ADRAW_COL_SILVER);
        for(x = maxLabelWidth + gOpt.border + gOpt.labelSep;
            x < w;
            x += gOpt.columnWidth * gOpt.divs)
        {
            drw->dottedLine(drw, x, gOpt.border, x, h - gOpt.border);
        }
        drw->setPen(drw, ADRAW_COL_BLACK);
    }

    /* Draw the elements */
    while(tw)
    {
        drw->textL(drw, maxLabelWidth + gOpt.border, y, tw->label);
        plotPattern(drw, maxLabelWidth + gOpt.border + gOpt.labelSep, y, tw->pattern);

        /* Advance down the page */
        y += gOpt.rowSep + gOpt.rowHeight;

        tw = tw->next;
    }
}

static void processGlobalOpts(const tig_attrlist_t *list)
{
    while(list)
    {
        switch(list->attr)
        {
            case TIG_GATTR_DIVS:
                gOpt.divs = atoi(list->val);
                break;
            default:
                /* Ignore unhandled */
                break;
        }

        list = list->next;
    }
}

/*****************************************************************************
 * Global Function Definitions
 *****************************************************************************/

int main(int argc, const char *argv[])
{
    FILE            *ismap = NULL;
    ADrawOutputType  outType;
    ADraw            drw;
    char            *outImage;
    tig_t           *tig;
    uint32_t         w, h;
    uint16_t         maxLabelWidth, maxPatternWidth;

    /* Parse the command line options */
    if(!CmdParse(gClSwitches, sizeof(gClSwitches) / sizeof(CmdSwitch), argc - 1, &argv[1], "-i"))
    {
        usage();
        return EXIT_FAILURE;
    }

    if(gDumpLicencePresent)
    {
        Licence();
        return EXIT_SUCCESS;
    }

    /* Check that the ouput type was specified */
    if(!gOutTypePresent)
    {
        fprintf(stderr, "-T <type> must be specified on the command line\n");
        usage();
        return EXIT_FAILURE;
    }

    /* Check that the output filename was specified */
    if(!gOutputFilePresent)
    {
        if(!gInputFilePresent || strcmp(gInputFile, "-") == 0)
        {
            fprintf(stderr, "-o <filename> must be specified on the command line if -i is not used or input is from stdin\n");
            usage();
            return EXIT_FAILURE;
        }

        gOutputFilePresent = true;
        snprintf(gOutputFile, sizeof(gOutputFile), "%s.%s", gInputFile, gOutType);
    }

    /* Determine the output type */
    if(strcmp(gOutType, "png") == 0)
    {
        outType  = ADRAW_FMT_PNG;
        outImage = gOutputFile;
    }
    else if(strcmp(gOutType, "eps") == 0)
    {
        outType  = ADRAW_FMT_EPS;
        outImage = gOutputFile;
    }
    else if(strcmp(gOutType, "svg") == 0)
    {
        outType  = ADRAW_FMT_SVG;
        outImage = gOutputFile;
    }
    else if(strcmp(gOutType, "ismap") == 0)
    {
#ifdef __WIN32__
        /* When building for Windows , use the 'dangerous' tempnam()
         *  as mkstemp() isn't present.
         */
        outType  = ADRAW_FMT_PNG;
        outImage = tempnam(NULL, "png");
        if(!outImage)
        {
            perror("tempnam() failed");
            return EXIT_FAILURE;
        }

        /* Schedule the temp file to be deleted */
        deleteTmpFilename = outImage;
        atexit(deleteTmp);
#else
        static char tmpTemplate[] = "/tmp/mscgenXXXXXX";
        int h;

        outType  = ADRAW_FMT_PNG;
        outImage = tmpTemplate;

        /* Create temporary file */
        h = mkstemp(tmpTemplate);
        if(h == -1)
        {
            perror("mkstemp() failed");
            return EXIT_FAILURE;
        }

        /* Close the file handle */
        close(h);

        /* Schedule the temp file to be deleted */
        deleteTmpFilename = outImage;
        atexit(deleteTmp);
#endif
    }
    else
    {
        fprintf(stderr, "Unknown output format '%s'\n", gOutType);
        usage();
        return EXIT_FAILURE;
    }

    /* Parse input, either from a file, or stdin */
    if(gInputFilePresent && !strcmp(gInputFile, "-") == 0)
    {
        FILE *in = fopen(gInputFile, "r");

        if(!in)
        {
            fprintf(stderr, "Failed to open input file '%s'\n", gInputFile);
            return EXIT_FAILURE;
        }
        tig = TigParse(in);
        fclose(in);
    }
    else
    {
        tig = TigParse(stdin);
    }

    /* Check if the parse was okay */
    if(!tig)
    {
        return EXIT_FAILURE;
    }

    /* Extract global options */
    processGlobalOpts(tig->gattr);

    /* Print the parse output if requested */
    if(gPrintParsePresent)
    {
        TigWaveDump(tig->waves);
    }

    /* Check if an ismap file should also be generated */
    if(strcmp(gOutType, "ismap") == 0)
    {
        ismap = fopen(gOutputFile, "w");
        if(!ismap)
        {
            fprintf(stderr, "Failed to open output file '%s':\n", gOutputFile);
            perror(NULL);
            return EXIT_FAILURE;
        }
    }

    /* Open the drawing context with dummy dimensions */
    if(!ADrawOpen(10, 10, outImage, outType, &drw))
    {
        fprintf(stderr, "Failed to create output context\n");
        return EXIT_FAILURE;
    }

    /* Compute page dimensions */
    computeDimensions(tig->waves, &drw, &w, &h, &maxLabelWidth, &maxPatternWidth);

    /* Close the temporary output file */
    drw.close(&drw);

    /* Open the output */
    if(!ADrawOpen(w, h, outImage, outType, &drw))
    {
        fprintf(stderr, "Failed to create output context\n");
        return EXIT_FAILURE;
    }

    plot(tig->waves, &drw, w, h, maxLabelWidth);

    drw.close(&drw);
}

/*****************************************************************************
 * Unit Test Support
 *****************************************************************************/


/* END OF FILE */

